package common;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Random;

import javax.sound.sampled.DataLine;

import peer.BitField;


/**Represents a packet. New (empty) Packets can be created, or Packets can be created from the packet received
 * from another host. Headers can be added, deleted and the value of headers can be retrieved, the data can be set
 * and retrieved, and the packet knows when it is valid (the hash code of data is generated automatically, as is 
 * comparing the hash code of data with the hash code header).
 * @author David Johnstone
 *
 */
public class Packet
{
	private static final String PROTOCOL_NAME = "NFP";
	private static final String CRLF = "\r\n";
	private static final String HASHING_ALGORITHM = "MD5";
	public static enum Type
    {
        nw_connect("00000000"),
        nw_connect_accept("00000010"),
        nw_connect_aok("00000011"),
        nw_ping("00000100"),
        nw_pong("00000110"),
        nw_aok("00000111"),
        nw_newpeers("00101100"),
        nw_newpeers_accept("00101110"),
        nw_newpeers_aok("00101111"),
        search("00010000"),
        found("00010100"),
        found_response("00010110"),
        found_response_aok("00010111"),
        request_file("00011000"),
        request_file_response("00011010"),
        request_file_response_aok("00011011"),
        status_request("00011100"),
        status_response("00011110"),
        status_response_aok("00011111"),
        connection_request("00100000"),
        connection_response("00100010"),
        connection_response_aok("00100011"),
        request_block("00100100"),
        data("00101000");
        private final byte type;
        Type(String bits)
        {
            this.type = Byte.parseByte(bits, 2);
        }
    };
    
	private boolean valid = true;
	private String problem;
	private byte type;
	private ArrayList<String> headers = new ArrayList<String>();
	private String data;
	private BitField bitField;
	
	
	/**Create a new packet object based on a packet that has been received. It automatically works out the
	 *  type, headers and data (if it has any). It then checks that the hash code of the data is the same
	 *  as the hash code given in the header.
	 * @param contents the packet as a string
	 */
	public Packet(String contents)
	{
		constructPacket(contents);
	}
	
	/**The same as the one above, but it takes an array of bytes (what you get with a datagram packet)
	 * I'm not sure if this is useful - you still need to work out the length somehow.
	 * Does datagramPacket.getLength() return the length of the array that is used or just the length of 
	 * the array you gave it? If it's only the length of the array you gave, it could make this a little
	 * more complicated - the unused bytes would probabably all be 0, so some sort of loop would have to go
	 * over the array in reverse to find where the data ends, and then another loop would go to that position
	 * and build a string from it.
	 * @param bytes
	 */
	public Packet(byte[] bytes)
	{
		int lastByte = bytes.length - 1;
		while(bytes[lastByte] == 0x00)
		{
			lastByte--;
		}
		StringBuilder sb = new StringBuilder();
		for(int i = 0; i <= lastByte; i++)
		{
			sb.append((char) bytes[i]);
		}
		String contents = sb.toString();
		constructPacket(contents);
	}

	private void constructPacket(String contents)
	{
		try
		{
			if(contents.length() >= 4 && contents.substring(0, 3).equals(PROTOCOL_NAME))
			{
				//work out what type of packet it is
				type = (byte) contents.charAt(3);
				//check if the packet is a data packet
				if(type == Type.data.type)
				{
					//find the data_length header
					int dlStart = contents.indexOf(CRLF + "data_length: ") + 2;
					int dlEnd = contents.indexOf(CRLF, dlStart);
					int dataLength = Integer.parseInt(contents.substring(dlStart + 13, dlEnd));
					//extract the data
					data = contents.substring(contents.length() - dataLength);
					//create an array of headers and add them to the headers
					String[] split = contents.substring(6, contents.length() - dataLength).split(CRLF);
					for (String s : split)
					{
						headers.add(s);
					}
					checkHashCodeWithData();
				}
				else if(type == Type.status_response.type)
				{
					//find the file_size header
					int fsStart = contents.indexOf(CRLF + "file_size: ") + 2;
					int fsEnd = contents.indexOf(CRLF, fsStart);
					long fileSize = Integer.parseInt(contents.substring(fsStart + 11, fsEnd));
					//work out the chunksize, then the number of chunks in the file, then the length of the data field in the received message
					int chunkSize = Util.getChunkSize(fileSize);
					int chunks = (int) (fileSize / chunkSize);
					if(fileSize % chunkSize > 0)
					{
						chunks++;
					}
					int dataLength = chunks / 8;
					if(dataLength % 8 > 0)
					{
						dataLength++;
					}
					//extract the data
					String rawData = contents.substring(contents.length() - dataLength);
					bitField = new BitField(rawData);
					//create an array of headers and add them to the headers
					String[] split = contents.substring(6, contents.length() - dataLength).split(CRLF);
					for (String s : split)
					{
						headers.add(s);
					}
				}
				else if (contents.length() >= 6)
				{
					String[] split = contents.substring(6).split(CRLF);
					for (String s : split)
					{
						headers.add(s);
					}
				}
				
			}
			else
			{
				valid = false;
				problem = "first 4 bytes of packet wrong";
			}
		} catch (NumberFormatException e)
		{
			valid = false;
			problem = "invalid numeric header value (possibly data_length or hash_code)";
		}
	}

	/**Makes sure the hash code of the data is the same as the hash code that was sent with the packet.
	 * If they are different, valid is set to false.
	 * @throws NoSuchAlgorithmException 
	 */
	private void checkHashCodeWithData()
	{
		try
		{
			//convert the header to a byte array
			//byte[] headerBytes = (new BigInteger(getHeader("hash_code"), 16)).toByteArray();
			String hashCode = getHeader("hash_code");
			byte[] headerBytes = new byte[hashCode.length() / 2];
			for (int i = 0; i < headerBytes.length; i++)
			{
				headerBytes[i] = (byte) Integer.parseInt(hashCode.substring(i*2, i*2+2), 16);
			}
			//calculate hash of data now
			MessageDigest md = MessageDigest.getInstance(HASHING_ALGORITHM);
			md.update(data.getBytes());
			byte[] dataBytes = md.digest();
			//compare them
			if(headerBytes.length != dataBytes.length)
			{
				valid = false;
				problem = "invalid hash_code header";
			}
			else
			{
				for (int i = 0; i < dataBytes.length; i++)
				{
					if(dataBytes[i] != headerBytes[i])
					{
						valid = false;
						problem = "hash_code header doesn't match data";
					}
				}
			}
		}
		catch (NoSuchAlgorithmException e)
		{
			System.out.println("No such hashing algorithm");
			e.printStackTrace();
		}
		
	}
	
	/**Create a new empty packet object to be sent.
	 * 
	 */
	public Packet()
	{}
	
	public Packet(Type t)
	{
		this.type = t.type;
	}

	public void setType(Type t)
	{
		type = t.type;
	}
	
	//don't use this
	public byte getType()
	{
		return type;
	}

	//use this instead
	//(I'm not entirely sure this code is correct)
	public boolean isType(Type t)
	{
		if(type == t.type)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	
	/**Adds a new header (assumes there are now new lines in the name or value)
	 * @param headerName name of the header
	 * @param value value of this header
	 */
	public void addHeader(String headerName, String value)
	{
		headers.add(headerName + ": " + value);
	}
	
	/**Remove all headers of a given name (not that there should be multiple headers sharing a name)
	 * @param headerName
	 */
	public void removeHeader(String headerName)
	{
		for(int i = 0; i < headers.size(); i++)
		{
			if(headers.get(i).startsWith(headerName))
			{
				headers.remove(i);
			}
		}
	}
	
	/**If this packet has a header of a given name.
	 * @param headerName name of header
	 * @return true if it has the header
	 */
	public boolean hasHeader(String headerName)
	{
		for (String header : headers)
		{
			if(header.startsWith(headerName))
			{
				return true;
			}
		}
		return false;
	}
	
	/**Returns the value of a header of a given name.
	 * @param headerName name of header
	 * @return value of header
	 */
	public String getHeader(String headerName)
	{
		String r = "";
		for (String header : headers) {
			if(header.startsWith(headerName))
			{
				r = header.substring(header.indexOf(": ") + 2);
			}
		}
		return r;
	}
	
	/**Set the data field for the packet (and automatically giving a hash code for it).
	 * This can only be called for data packets (as they are the only ones allowed to have a data field)
	 * @param data 
	 */
	public void setData(String data)
	{
		if(type == Type.data.type)
		{
			if(data != null)
			{
				this.data = data;
				addDataHash(data);
			}
			else
			{
				this.data = "";
			}
		}
	}
	
	public void setData(byte[] d)
	{
		//this may not work, or, it may not be the best way to do it
		StringBuilder sb = new StringBuilder();
		for (byte b : d)
		{
			sb.append(b);
		}
		String data = sb.toString();
		if(type == Type.data.type)
		{
			if(data != null)
			{
				this.data = data;
				addDataHash(data);
			}
			else
			{
				this.data = "";
			}
		}
	}
	
	private void addDataHash(String data)
	{
		try
		{
			//calculate hash
			MessageDigest md = MessageDigest.getInstance(HASHING_ALGORITHM);
			md.update(data.getBytes());
			//convert it to a hex string
			byte[] md5hash = md.digest();
			StringBuilder sb = new StringBuilder();
			for (byte b : md5hash)
			{
				String a = Integer.toHexString(b & 0xFF);
				if(a.length() == 1) //as Integer.toHexString doesn't give leading 0s
				{
					sb.append("0" + a);
				}
				else
				{
					sb.append(a);
				}
			}
			//remove previous headers
			removeHeader("hash_code");
			//add it as a header
			addHeader("hash_code", sb.toString());
		}
		catch (NoSuchAlgorithmException e)
		{
			System.out.println("No such hashing algorithm");
			e.printStackTrace();
		}
	}
	
	/**Randomly corrupts bytes of the data field. There is a corruptionRate * 100 % chance a byte will be set to
	 * the value of a new random byte.
	 * @param corruptionRate the probability (as a number between 0 and 1) any single byte will be changed
	 * @return true if the packet has been altered (corrupted)
	 */
	public boolean corruptData(double corruptionRate)
	{
		boolean corrupted = false;
		if(type == Type.data.type)
		{
			Random random = new Random();
			byte[] dataBytes = data.getBytes();
			for(int i = 0; i < dataBytes.length; i++)
			{
				//randomly select bytes to corrupt
				if(random.nextFloat() < corruptionRate)
				{
					//set chosen byte to a random byte  
					dataBytes[i] = (byte) random.nextInt(255);
					corrupted = true;
				}
			}
			StringBuilder sb = new StringBuilder();
			for (byte b : dataBytes)
			{
				sb.append((char) b);
			}
			data = sb.toString();
		}
		return corrupted;
	}

	/**Gets the data field
	 * @return the data
	 */
	public String getData()
	{
		if(type == Type.data.type)
		{
			return data;
		}
		else
		{
			return null;
		}
	}
	
	/**Set the bit field for this packet (provided this is a status_response message)
	 * @param bitField
	 */
	public void setBitField(BitField bitField)
	{
		if(type == Type.status_response.type)
		{
			this.bitField = bitField;
		}
	}
	
	/**Get the bit field for this packet
	 * @return
	 */
	public BitField getBitField()
	{
		if(type == Type.status_response.type)
		{
			return bitField;
		}
		else
		{
			return null;
		}
	}
	
	/**Returns the packet as a string so it can be sent in a packet
	 * @return the packet
	 */
	public String toString()
	{
		String s = PROTOCOL_NAME + (char) type;
		for (String header : headers) {
			s += CRLF + header;
		}
		if(type == Type.data.type)
		{
			s += CRLF + data;
		}
		if(type == Type.status_response.type)
		{
			s += CRLF + bitField.toString();
		}
		return s;
	}
	
	public boolean isValid()
	{
		return valid;
	}
	
	/**Returns a string with the reason this packet is invalid
	 * @return
	 */
	public String getProblem()
	{
		return problem;
	}

	public byte[] getBytes()
	{
		return this.toString().getBytes();
	}
}
